
/****************************************************************************/
/* Copyright 2008 Jesse E. H-K                                              */
/*                                                                          */
/* This file is part of Icepac.                                             */
/*                                                                          */
/* Icepac is free software: you can redistribute it and/or modify           */
/* it under the terms of the GNU General Public License as published by     */
/* the Free Software Foundation, either version 3 of the License, or        */
/* (at your option) any later version.                                      */
/*                                                                          */
/* Icepac is distributed in the hope that it will be useful,                */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of           */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            */
/* GNU General Public License for more details.                             */
/*                                                                          */
/* You should have received a copy of the GNU General Public License        */
/* along with Icepac.  If not, see <http://www.gnu.org/licenses/>.          */
/****************************************************************************/

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.h>

#include <libgen.h>
#include <argp.h>

#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>

#define BUFFSIZE 256

static lua_State *L = NULL;

#define DIRMAX 512
#define PKGMAX 128
#define RESULTS_MAX 50000

/*
 * These represent the equivalent entries
 * in ~/.icepac.
 */
static char ABS_ROOT[DIRMAX];
static char LOCAL_DIR[DIRMAX];

/*
 * Print a formatted error message and quit.
 */
static void error( const char *fmt, ... ) {
    va_list argp;
    va_start( argp, fmt );
    
    if ( fmt != NULL ) {
        fprintf( stderr, PACKAGE ": " );
        vfprintf( stderr, fmt, argp );
    }
    
    va_end( argp );
    exit( EXIT_FAILURE );
}

static void *xmalloc( size_t bytes ) {
    void *m = malloc( bytes );
    
    if ( m == NULL )
        error( "%s\n", "Memory allocation error" );
    
    return m;
}

/*
 * Retrieve <pkg>.<item> assuming it is a string.
 */
static const char *getfield( const char *pkg, const char *item ) {
    lua_getglobal( L, pkg );
    
    lua_pushstring( L, item );
    lua_gettable( L, -2 );
    return lua_tostring( L, -1 );
}

/*
 * Set <pkg>.<item> to <value> assuming all three are strings.
 * 
 * example use: set package.path to "/home/joe"
 * setfield( "package", "path", "/home/jesse" )
 */
static void setfield( const char *pkg, const char *key, const char *value ) {
    lua_getglobal( L, pkg );    
    lua_pushstring( L, key );
    lua_pushstring( L, value );
    lua_settable( L, -3 );
}

/*
 * A single package.
 * Contains information on
 * its index in the search query,
 * its location in the filesystem,
 * and its name.
 */
typedef struct pnode {
    int index;               /* Index among search results */
    char loc[DIRMAX + 1];    /* Location in filesystem */
    char name[PKGMAX + 1];   /* Package name */
    
    struct pnode *next;      /* IMPLEMENTATION: Next package node */
    int _used;               /* IMPLEMENTATION: Tag for memory allocation */
} pnode;

/* Memory for the list of matching packages. */
static pnode sharedmem[RESULTS_MAX];

/* 
 * Allocate memory for a single package node.
 * Handles all memory management internally using a simple algorithm.
 * Memory no longer in used will be recycled.
 */
static pnode *allocator( void ) {
    int i;
    
    for ( i = 0; i < RESULTS_MAX; i++ ) {
        if ( ! sharedmem[i]._used) {
            sharedmem[i]._used = 1;
            return &(sharedmem[i]);
        }
    }
    
    error( "The number of matching packages has exceed the maximum of %d\n", RESULTS_MAX );
}

static pnode *pnode_make( int index, const char *loc, const char *name, pnode *next ) {
    pnode *n = allocator();
    
    n->index = index;
    strcpy( n->loc, loc );
    strcpy( n->name, name );
    
    n->next = next;
    return n;
}

static pnode *pnode_destroy( pnode *node ) {
    pnode *next = node->next;
    node->_used = 0;
    
    return next;
}

/*
 * List of indexed packages.
 */
typedef struct plist {
    pnode *head;         /* Head of the list */
    int ip;              /* Maintained index */
} plist;

/* A few macros to ease working with package lists/nodes */
#define PNEXT( n ) ((n)->next)
#define PEND() NULL
#define PBEGIN( l ) ((l)->head)
#define PCOUNT( l ) ((l)->ip)

#define PINDEX( n ) ((n)->index)
#define PLOC( n ) ((n)->loc)
#define PNAME( n ) ((n)->name)

static plist *plist_make( void ) {
    plist *l = xmalloc( sizeof *l );
   
    l->head = NULL;
    l->ip = 1;
    return l;
}

static void plist_add( plist *l, const char *loc, const char *name ) {
    pnode *iter = NULL;
    
    if ( l->head == NULL )
        l->head = pnode_make( l->ip++, loc, name, NULL );
    else {
        iter = l->head;
        while ( iter->next != NULL )
            iter = iter->next;
        
        iter->next = pnode_make( l->ip++, loc, name, NULL );
    }
}

static void plist_destroy( plist *l ) {
    pnode *iter = NULL;
    
    iter = l->head;
    while ( iter != NULL )
        iter = pnode_destroy( iter );
    
    free( l );
}

/*
 * Initiate a call to a function in the icepac library
 * where "name" refers to function "icepac.<name>"
 * by pushing it onto the global lua stack.
 */
static void initfunc( const char *name ) {
    lua_getglobal( L, "icepac" );
    lua_pushstring( L, name );
    lua_gettable( L, -2 );
}

/*
 * Check to make sure that
 * the package tree location specified in
 * the configuration file contains all the
 * repositories specified.
 * 
 * Returns true or false.
 */
static int checktree( void ) {
    initfunc( "checktree" );
    /* lua_pushstring( L, ABS_ROOT ); */

    if ( lua_pcall( L, 0, 1, 0 ) )
        error( "%s\n", lua_tostring( L, -1 ) );
    
    return lua_toboolean( L, -1 );
}

static const char *locatepackage( const char *name ) {
    const char *ent = NULL;

    initfunc( "locate" );
    lua_pushstring( L, name );

    if ( lua_pcall( L, 1, 1, 0 ) )
        error( "%s\n", lua_tostring( L, -1 ) );

    if ( lua_isnil( L, -1 ) )
        return NULL;
    else
        return lua_tostring( L, -1 );
}   

/*
 * Return a plist based on matches to the
 * "desired" pattern.
 */
static plist *searchpackage( const char *desired ) {
    const char *ent = NULL;
    char buff[DIRMAX];
    plist *pl = NULL;
    
    initfunc( "search" );
    lua_pushstring( L, desired );
    
    if ( lua_pcall( L, 1, 1, 0 ) )
        error( "%s\n", lua_tostring( L, -1 ) );
    
    if ( lua_isnil( L, -1 ) )
        return NULL;
    
    pl = plist_make();
    /* Start with a nil. This is convention. */
    lua_pushnil( L );
    while ( lua_next( L, -2 ) ) {
        ent = luaL_checkstring( L, -1 );
        strcpy( buff, ent );
        
        plist_add( pl, ent, basename( buff ) );
        /* Leave the key on the stack for the next round,
         * but pop the value. */
        lua_pop( L, 1 );
    }
    
    /* Pop the list of entries */
    lua_pop( L, 1 );
    return pl;
}

static const char *getpath( void ) {
    return getfield( "package", "path" );
}

static void setpath( void ) {
    char buffer[1024];
    
    if ( LOCAL_BUILD ) {
        sprintf( buffer, "%s;%s", "./?.lua", "./?" );
        setfield( "package", "cpath", "./?.so" );
        setfield( "package", "path", buffer );
    }
}

#define STRINGIFY( var ) #var

#define checktable( table )                                             \
    {                                                                   \
        lua_getglobal( L, STRINGIFY( table ) );                         \
        if ( ! lua_istable( L, -1 ) )                                   \
            error( "Table expected for " STRINGIFY( table) " in ~/.icepac\n" ); \
    }

#define checkstring( var )                                              \
    {                                                                   \
        lua_getglobal( L, STRINGIFY( var ) );                           \
        if ( ! lua_isstring( L, -1 ) )                                  \
            error( "String expected for " STRINGIFY( var ) " in ~/.icepac\n" ); \
        strcpy( var, lua_tostring( L, -1 ) );                           \
    }

#define checkdir( var )                                                 \
    {                                                                   \
        initfunc( "isdir" );                                            \
        checkstring( var );                                             \
        if ( lua_pcall( L, 1, 1, 0 ) || ! lua_toboolean( L, -1 ) )      \
            error( STRINGIFY( var ) " in ~/.icepac is not a valid directory\n" ); \
    }

static void readconfig( void ) {
    char buffer[512];
    
    sprintf( buffer, "%s/%s", getenv( "HOME" ), ".icepac" );
    
    if ( luaL_dofile( L, buffer ) ) {
        error( "%s\n", lua_tostring( L, -1 ) );
    }

    checkdir( ABS_ROOT );
    checkstring( LOCAL_DIR );
    checktable( REPOS );
}

static const char *cppackage( const char *name, const char *loc ) {
    initfunc( "cppackage" );
    
    lua_pushstring( L, name );
    lua_pushstring( L, loc );
    
    if ( lua_pcall( L, 2, 1, 0 ) )
        error( "%s\n", lua_tostring( L, -1 ) );
    
    return lua_tostring( L, -1 );
}

static int confirm( const char *prompt ) {
    char buff[BUFFSIZE];

    printf( "%s", prompt );
    if ( fgets( buff, BUFFSIZE, stdin ) == NULL )
        error( "Error reading input\n" );
    
    return toupper( buff[0] ) == 'Y';
}

#if LOCAL_BUILD
#  define LOGIC_LOC "."
#else
#  define LOGIC_LOC LUA_LDIR
#endif

#define ABOUT_STR \
    "\nThis is " PACKAGE_STRING "\n"            \
    "Copyright 2008 Jesse E. H-K\n\n"

static char args_doc[] = "<package or pattern>";

static struct argp_option options[] = {
    { "search", 's', 0, 0, "Search mode" },
    { "noconfirm", 'n', 0, 0, "Don't confirm actions" },
    { "quiet", 'q', 0, 0, "Don't print any output. Can be compined with --noconfirm" },
    { 0 },
};

#define PKG_MAX 5000

struct arguments {
    int search;
    int noconfirm;
    int quiet;

    char *args[PKG_MAX];
    int numbpkg;
};

static error_t parseopt( int key, char *arg, struct argp_state *state ) {
    struct arguments *arguments = state->input;

    switch ( key ) {
    case 's':
        arguments->search = 1;
        break;
    case 'n':
        arguments->noconfirm = 1;
        break;
    case 'q':
        arguments->quiet = 1;
        break;
    case ARGP_KEY_ARG:
        if ( ((state->arg_num >= 1) && arguments->search) || (state->arg_num > PKG_MAX) )
            /* Too many arguments. */
            argp_usage( state );

        arguments->args[state->arg_num] = arg;
        
        break;
    case ARGP_KEY_END:
        /* Not enough arguments! */
        if ( state->arg_num < 1 )
            argp_usage( state );

        break;
    default:
        return ARGP_ERR_UNKNOWN;
    }

    arguments->numbpkg = state->arg_num;
    return 0;
}

static struct argp argp = { options, parseopt, args_doc, ABOUT_STR };

static plist *PKGL = NULL;

static void cleanup( void ) {
    lua_close( L );
    
    if ( PKGL )
        plist_destroy( PKGL );

}

static int getwidth( int numbresults) {
    int count = 1;
    
    while ( numbresults >= 10 ) {
        ++count;
        numbresults /= 10;
    }

    return count;
}

static void searchmode( const char *pattern ) {
    pnode *iter = NULL;
    int width;
    char buffer[BUFFSIZE];

    if ( (PKGL = searchpackage( pattern )) == NULL )
        return;
    
    width = getwidth( PCOUNT( PKGL ) );
    iter = PBEGIN( PKGL );
    for (; iter != PEND(); iter = PNEXT( iter ) )
        printf( "%-*d: %s\n", width, PINDEX( iter ), PNAME( iter ) );
}   

static void installmode( struct arguments *arguments ) {
    const char *pkgname = NULL;
    const char *loc = NULL;
    const char *savedloc = NULL;
    char buff[BUFFSIZE];
    int i;

    for ( i = 0; i < arguments->numbpkg; i++ ) {
        pkgname = arguments->args[i];
        loc = locatepackage( pkgname );
        if ( loc == NULL )
            error( "No such package: %s\n", pkgname );

        if ( ! arguments->noconfirm ) {
            sprintf( buff, "Copy %s to local directory? [y/N] ", pkgname );
            if ( ! confirm( buff ) )
                continue;
        }
        
        savedloc = cppackage( pkgname, loc );
        if ( ! arguments->quiet )
            printf( "%s copied from %s to %s\n", pkgname, loc, savedloc );
    }
}

int main( int argc, char **argv ) {
    struct arguments arguments;    

    arguments.search = 0;
    arguments.noconfirm = 0;
    arguments.quiet = 0;
    
    L = lua_open();
    
    /* 
     * Make sure the Lua instance is destroyed
     * when the program exits from now on.
     */
    atexit( cleanup );

    luaL_openlibs( L );    

    setpath();
    
    if ( luaL_dofile( L, LOGIC_LOC "/icepac.lua" ) )
        error( "Unable to read necessary file icepac.lua\n"
               "%s\n", lua_tostring( L, -1 ) );
    
    
    /* Read config file and set important vars. */
    readconfig();
    
    /*
     * Check the abs tree for all the repositories mentioned in the
     * configuration file.
     */
    if ( ! checktree() )
        error( "Unable to locate a valid ABS tree at existing directory %s\n"
               "Some repositories specified in ~/.icepac are not present\n", ABS_ROOT );
    
    argp_parse( &argp, argc, argv, 0, 0, &arguments );    

    if ( arguments.search )
        searchmode( arguments.args[0] );
    else
        installmode( &arguments );
    
    return 0;
}
